/*
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.imagepipeline.memory;

import java.util.concurrent.Semaphore;

import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.Throwables;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.memory.MemoryTrimType;
import com.facebook.common.memory.MemoryTrimmable;
import com.facebook.common.memory.MemoryTrimmableRegistry;
import com.facebook.common.references.CloseableReference;
import com.facebook.common.references.OOMSoftReference;
import com.facebook.common.references.ResourceReleaser;

/**
 * Maintains a shareable reference to a byte array.
 * 
 * <p>
 * When accessing the shared array proper synchronization is guaranteed. Under
 * hood the get method acquires an exclusive lock, which is released whenever
 * the returned CloseableReference is closed.
 * 
 * <p>
 * If the currently available byte array is too small for a request it is
 * replaced with a bigger one.
 * 
 * <p>
 * This class will also release the byte array if it is unused and collecting it
 * can prevent an OOM.
 */

public class SharedByteArray implements MemoryTrimmable {
	@VisibleForTesting
	final int mMinByteArraySize;
	@VisibleForTesting
	final int mMaxByteArraySize;

	/**
	 * The underlying byte array.
	 * 
	 * <p>
	 * If we receive a memory trim notification, or the runtime runs pre-OOM gc
	 * it will be cleared to reduce memory pressure.
	 */
	@VisibleForTesting
	final OOMSoftReference<byte[]> mByteArraySoftRef;

	/**
	 * Synchronization primitive used by this implementation
	 */
	@VisibleForTesting
	final Semaphore mSemaphore;

	private final ResourceReleaser<byte[]> mResourceReleaser;

	public SharedByteArray(MemoryTrimmableRegistry memoryTrimmableRegistry, PoolParams params) {
		Preconditions.checkNotNull(memoryTrimmableRegistry);
		Preconditions.checkArgument(params.minBucketSize > 0);
		Preconditions.checkArgument(params.maxBucketSize >= params.minBucketSize);

		mMaxByteArraySize = params.maxBucketSize;
		mMinByteArraySize = params.minBucketSize;
		mByteArraySoftRef = new OOMSoftReference<byte[]>();
		mSemaphore = new Semaphore(1);
		mResourceReleaser = new ResourceReleaser<byte[]>() {
			@Override
			public void release(byte[] unused) {
				mSemaphore.release();
			}
		};

		memoryTrimmableRegistry.registerMemoryTrimmable(this);
	}

	/**
	 * Get exclusive access to the byte array of size greater or equal to the
	 * passed one.
	 * 
	 * <p>
	 * Under the hood this method acquires an exclusive lock that is released
	 * when the returned reference is closed.
	 */
	public CloseableReference<byte[]> get(int size) {
		Preconditions.checkArgument(size > 0, "Size must be greater than zero");
		Preconditions.checkArgument(size <= mMaxByteArraySize, "Requested size is too big");
		mSemaphore.acquireUninterruptibly();
		try {
			byte[] byteArray = getByteArray(size);
			return CloseableReference.of(byteArray, mResourceReleaser);
		} catch (Throwable t) {
			mSemaphore.release();
			throw Throwables.propagate(t);
		}
	}

	private byte[] getByteArray(int requestedSize) {
		final int bucketedSize = getBucketedSize(requestedSize);
		byte[] byteArray = mByteArraySoftRef.get();
		if (byteArray == null || byteArray.length < bucketedSize) {
			byteArray = allocateByteArray(bucketedSize);
		}
		return byteArray;
	}

	/**
	 * Responds to memory pressure by simply 'discarding' the local byte array
	 * if it is not used at the moment.
	 * 
	 * @param trimType
	 *            kind of trimming to perform (ignored)
	 */
	@Override
	public void trim(MemoryTrimType trimType) {
		if (!mSemaphore.tryAcquire()) {
			return;
		}
		try {
			mByteArraySoftRef.clear();
		} finally {
			mSemaphore.release();
		}
	}

	@VisibleForTesting
	int getBucketedSize(int size) {
		size = Math.max(size, mMinByteArraySize);
		return Integer.highestOneBit(size - 1) * 2;
	}

	private synchronized byte[] allocateByteArray(int size) {
		// Start with clearing reference and releasing currently owned byte
		// array
		mByteArraySoftRef.clear();
		byte[] byteArray = new byte[size];
		mByteArraySoftRef.set(byteArray);
		return byteArray;
	}
}
